﻿using System;

namespace ArtheaEngine
{
    public enum MoneyType
    {
        Silver,
        Gold,
        Platinum
    }

    public class Money : IEquatable<Money>, IFormattable
    {
        private readonly decimal[] amount = new decimal[Enum.GetValues(typeof (MoneyType)).Length];

        public Money(decimal value, MoneyType type)
        {
            this[type] = value;
        }

        public Money(Money from)
        {
            foreach (MoneyType type in Enum.GetValues(typeof (MoneyType)))
            {
                this[type] = from[type];
            }
        }

        public Money()
        {
        }

        public decimal this[MoneyType type]
        {
            get { return amount[(int) type]; }
            set
            {
                amount[(int) type] = value;
                Normalize();
            }
        }

        #region IEquatable<Money> Members

        public bool Equals(Money money)
        {
            if (money == null) return false;
            for (int i = 0; i < amount.Length; i++)
            {
                if (amount[i] != money.amount[i])
                    return false;
            }
            return true;
        }

        #endregion

        #region IFormattable Members

        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (format == null) format = "G";

            switch (format.ToUpper())
            {
                case "G":
                    return this[MoneyType.Gold].ToString("F2");
                case "S":
                    return this[MoneyType.Silver].ToString("F2");
                case "P":
                    return this[MoneyType.Platinum].ToString("F2");
                default:
                    throw new FormatException("Invalid money format");
            }
        }

        #endregion

        public static implicit operator decimal(Money m)
        {
            return m.ValueAs(MoneyType.Gold);
        }

        public static implicit operator Money(decimal value)
        {
            return new Money(value, MoneyType.Gold);
        }

        public static Money operator -(Money a, Money b)
        {
            return new Money(a).Subtract(b);
        }

        public static bool operator !=(Money a, Money b)
        {
            return !(a == b);
        }

        public static Money operator +(Money a, Money b)
        {
            return new Money(a).Add(b);
        }

        public static bool operator ==(Money a, Money b)
        {
            return a.Equals(b);
        }

        public static bool operator >=(Money a, Money b)
        {
            return a.Has(b);
        }

        public static bool operator <=(Money a, Money b)
        {
            return (a == b) || (!a.Has(b));
        }

        public static bool operator >(Money a, Money b)
        {
            return !b.Has(a);
        }

        public static bool operator <(Money a, Money b)
        {
            return !a.Has(b);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(this, obj)) return true;
            return Equals(obj as Money);
        }

        public override int GetHashCode()
        {
            return amount != null ? amount.GetHashCode() : 0;
        }

        private static decimal GetMod(int type)
        {
            decimal total = 1;
            for (int i = type; i > 0; i--)
                total *= 100;
            return total;
        }

        private void Normalize()
        {
            Array values = Enum.GetValues(typeof (MoneyType));
            for (int i = 0; i < values.Length - 1; i++)
            {
                for (int j = values.Length - 1; j > i; j--)
                {
                    decimal mod = GetMod(j);
                    while (amount[i] > mod)
                    {
                        amount[i] -= mod;
                        amount[j] += 1;
                    }
                }
            }
        }

        public Money Add(Money from)
        {
            for (int i = 0; i < amount.Length; i++)
                amount[i] += from.amount[i];

            Normalize();
            return this;
        }

        public decimal Add(decimal value, MoneyType type)
        {
            this[type] += value;

            Normalize();

            return this[type];
        }

        public bool Has(ulong value, MoneyType type)
        {
            return this[type] >= value;
        }

        public bool Has(Money value)
        {
            for (int i = amount.Length - 1; i >= 0; i++)
            {
                if (amount[i] < value.amount[i])
                    return false;
            }
            return true;
        }

        public Money Subtract(Money from)
        {
            foreach (MoneyType type in Enum.GetValues(typeof (MoneyType)))
                Subtract(from[type], type);

            return this;
        }

        public decimal Subtract(decimal value, MoneyType type)
        {
            int next = (int) type + 1;

            while (this[type] < value && next < amount.Length)
            {
                if (amount[next] > 0)
                    amount[next++]--;

                this[type] += 100;
            }

            this[type] -= Math.Min(this[type], value);

            return this[type];
        }

        /// <summary>
        /// Values as.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public decimal ValueAs(MoneyType type)
        {
            decimal total = this[type];
            int count = 1;

            for (int i = (int) type + 1; i < amount.Length; i++)
            {
                decimal mod = GetMod(count++);
                total += amount[i]*mod;
            }
            return total;
        }
    }
}